"""YUI media support.

This is a clone of logic in yuiloader.js.
"""

from django.conf import settings

from hcp.media import Media, MediaMiddleware
from hcp.media.yui_components import COMPONENTS

#YUI_BASE = settings.YUI_BASE
YUI_ROOT = '2.7.0/build/'
YUI_BASE = 'http://yui.yahooapis.com/' + YUI_ROOT
YUI_COMBO_BASE = 'http://yui.yahooapis.com/combo?'
# XXX: Set up defaults for all the other YUI options in settings.

VERSIONS = {
    'raw': '',
    'min': '-min',
    'debug': '-debug',
}

SKIN_PREFIX = 'skin-'

class YUIMedia(Media):

    def __init__(self):
        super(YUIMedia, self).__init__()
        self.components = COMPONENTS()
        self.required = set()
        if settings.DEBUG:
            self.version = 'debug'
        else:
            self.version = 'min'
        self.optional = False
        self.combine = True
        self.rollup = True
        self.base = YUI_BASE
        self.skin = {
            # Default skin to use if no override for a component.
            'default_skin': 'sam',
            # Base combined with the YUI base URL.
            'base': 'assets/skins/',
            # Use this CSS instead if more than rollup_count CSS files are used.
            'rollup_name': 'skin.css',
            # Number of CSS files that will cause it to use rollup_name.
            'rollup_count': 3,
            # Skin overrides.
            # The key is the component name.
            # The value is a list of skin names.
            'overrides': {},
            # CSS files that must appear before the skin file.
            'after': ['reset', 'fonts', 'grids', 'base'],
        }

        # XXX: if version != min, rollup must be False
        # Conversely, turning on Rollups must set version to 'min'
        # All rollups are minified.
        # XXX: If base != YUI_BASE, then combine must be False
        # (combo URL's only work with yahoo CDN)
        self._paths = []

    def include_path(self, path):
        # Defer checking of path type until finalize.  This is so that we
        # can have commands (NotYetImplemented) for adding custom components.
        self._paths.append(path)
        return ''

    def finalize(self):
        self._config()
        self._setup()
        self._add_paths()
        self._explode()
        if self.rollup:
            self._rollup()
        self._reduce()
        self._sort()
        self._finalize()

    def _add_paths(self):
        for path in self._paths:
            if path in self.components:
                self.required.add(path)



    def _config(self):
        if self.version == 'debug':
            # Logger is required with debug.
            self.include_path('logger')

    def _setup(self):
        # Create skin modules.
        for component_name, component in self.components.items():
            if component.get('skinnable', False):
                overrides = self.skin['overrides'].get(component_name, [])
                if overrides:
                    for override in overrides:
                        smod = self._add_skin(override, component)
                else:
                    smod = self._add_skin(self.skin['default_skin'], component)

                component.setdefault('requires', []).append(smod)

    def _add_skin(self, skin, component):
        skin_name = self.format_skin(skin)
        if skin_name not in self.components:
            skin_path = self.skin['base'] + skin + '/' + self.skin['rollup_name']
            self.add_component(name=skin_name, type='css', path=skin_path,
                               after=self.skin['after'], rollup=self.skin['rollup_name'],
                               ext=component.get('ext', False))

        component_name = component['name']
        skin_name = self.format_skin(skin, component_name)
        if skin_name not in self.components:
            pkg = component.get('pkg', component_name)
            skin_path = pkg + '/' + self.skin['base'] + skin + '/' + component_name + '.css'
            self.add_component(name=skin_name, type='css', path=skin_path,
                               after=self.skin['after'], ext=component.get('ext', False))

        return skin_name


    def add_component(self, name, type, path, requires=None, optional=None,
                      supersedes=None, after=None, rollup=None, fullpath=None,
                      skinnable=False, ext=True, pkg=None):
        # XXX: What is ext? External?
        if requires is None:
            requires = []
        c = {
            'name': name,
            'type': type,
            'path': path,
            'skinnable': skinnable,
            'ext': ext,
            'requires': requires,
        }
        if optional:
            c['optional'] = optional
        if supersedes:
            c['supersedes'] = supersedes
        if after:
            c['after'] = after
        if rollup:
            c['rollup'] = rollup
        if fullpath:
            c['fullpath'] = fullpath
        if pkg:
            c['pkg'] = pkg
        self.components[name] = c

    def _explode(self):
        """Add all components that are required."""
        # Convert to list to handle updating while iterating.
        for component_name in list(self.required):
            component = self.components[component_name]
            required = self.get_requires(component)
            self.required.update(required)

    def _rollup(self):
        rollups = {}
        # Find and cache rollup components.
        for component_name, component in self.components.items():
            if component.get('rollup', 0):
                rollups[component_name] = component


        # Make as many passes as needed to pick up rollup rollups.
        while 1:
            rolled = False

            # Go through the rollup candidates.
            for rollup_name, rollup_component in rollups.items():
                if rollup_name not in self.required:
                    if not rollup_component.get('rollup', 0):
                        continue

                    roll = False
                    count = 0
                    if rollup_component.get('ext', False) and self.parse_skin(rollup_name):
                        for required_name in self.required:
                            if required_name != rollup_name and self.parse_skin(required_name):
                                count += 1
                                roll = count >= rollup_component.get('rollup', 0)
                                if roll:
                                    break
                    else:
                        for supersede in rollup_component.get('supersedes', ()):
                            if supersede in self.required:
                                count += 1
                                roll = count >= rollup_component.get('rollup', 0)
                                if roll:
                                    break

                    if roll:
                        self.required.add(rollup_name)
                        rolled = True
                        # Expand the rollup's dependencies.
                        self.get_requires(rollup_component)

            # If we made it here w/o rolling up something, we are done.
            if not rolled:
                break

    def _reduce(self):
        """Remove superceded modules."""
        # Convert to list to handle modification while iterating.
        for component_name in list(self.required):
            # Remove anything this module supersedes.
            skin_def = self.parse_skin(component_name)
            if skin_def:
                if not skin_def[1]:
                    skin_pre = SKIN_PREFIX + skin_def[0]
                    for required_name in list(self.required):
                        required_component = self.components[required_name]
                        ext = required_component.get('ext', False)
                        if (not ext and required_name != component_name and
                            required_name.find(skin_pre) > -1
                           ):
                            self.required.remove(required_name)
                            # XXX: Modify while iter.
            else:
                component = self.components[component_name]
                for supersede in component.get('supersedes', ()):
                    if supersede in self.required:
                        self.required.remove(supersede)

    def _sort(self):
        """Return a sorted order list of modules based on dependencies."""
        def requires(comp, other_name):
            other = self.components[other_name]
            expanded = comp.get('expanded', ())
            if other_name in expanded:
                return True
            after = comp.get('after', ())
            if other_name in after:
                return True
            optional = comp.get('optional', ())
            if self.optional and other_name in optional:
                return True

            # Check if this module requires one of the other supersedes.
            for supersede in other.get('supersedes', ()):
                if requires(comp, supersede):
                    return True

            # External css files should be sorted below yui css.
            if (comp.get('ext', False) and comp['type'] == 'css' and
                not other.get('ext', False) and other['type'] == 'css'
               ):
                return True

            return False

        start_index = 0
        sorted = list(self.required)
        while 1:
            moved = False
            index = start_index
            while index < len(sorted):
                sort = sorted[index]
                # Check everything below the current item.
                # Move if we find a requirement for the current item.
                below_index = index+1
                while below_index < len(sorted):
                    if requires(self.components[sort], sorted[below_index]):
                        # Move this dependency.
                        depend = sorted.pop(below_index)
                        sorted.insert(index, depend)
                        moved = True
                        break
                    below_index += 1
                if moved:
                    break
                else:
                    start_index += 1
                index += 1

            if not moved:
                break
        self.sorted = sorted

    def _version(self, path):
        return path.replace('-min.js', VERSIONS[self.version] + '.js')

    def _finalize(self):
        if self.combine:
            media = {'js': [], 'css': []}
            for name in self.sorted:
                component = self.components[name]
                if component.get('ext', False):
                    self.media[component['type']].append(self._url(component))
                else:
                    path = YUI_ROOT + component['path']
                    media[component['type']].append(self._version(path))
            if media['css']:
                path = YUI_COMBO_BASE + '&'.join(media['css'])
                self.media['css'].append(path)
            if media['js']:
                path = YUI_COMBO_BASE + '&'.join(media['js'])
                self.media['js'].append(path)
        else:
            for name in self.sorted:
                component = self.components[name]
                self.media[component['type']].append(self._url(component))
        # XXX: This is kinda hackish.
        # Non-YUI are always at the end.
        for path in self._paths:
            if path not in self.components:
                super(YUIMedia, self).include_path(path)


    def _url(self, component):
        path = YUI_BASE + component['path']
        versioned = self._version(path)
        return path

    def get_requires(self, component):
        requires = component.setdefault('requires', [])
        result = set()
        for require in requires:
            result.add(require)
            req_component = self.components[require]
            result.update(self.get_requires(req_component))

        if self.optional:
            for optional in component.get('optional', ()):
                result.add(optional)
                opt_component = self.components[optional]
                result.update(self.get_requires(opt_component))

        result = list(result)
        component['expanded'] = result
        return result

    def format_skin(self, skin, component_name=None):
        result = SKIN_PREFIX + skin
        if component_name:
            result = result + '-' + component_name
        return result

    def parse_skin(self, skin):
        # Return (skin_name, module).  Module may be None.
        # Return None
        if skin.find(SKIN_PREFIX) == 0:
            a = skin.split('-')
            if len(a) > 2:
                module = a[2]
            else:
                module = None
            return (a[1], module)
        return None

class YUIMiddleware(MediaMiddleware):

    def media(self):
        return YUIMedia


#type: 'js' or 'css'
#path: 'path/to/foo.js'
#requires: Other components this requires.
#optional: Optional components.
#skinnable: Whether or not skinnable.
#after: Must appear after these components.
#supersedes: Components this replaces.
#pkg: CSS package name for JS modules (defaults to the module name).
#rollup: XXX not used (for CSS rollups)
